// Copyright 2019 Yunion
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package alerting

import (
	"context"
	"regexp"
	"strconv"
	"time"

	"yunion.io/x/jsonutils"
	"yunion.io/x/pkg/errors"

	"yunion.io/x/onecloud/pkg/apis/monitor"
	"yunion.io/x/onecloud/pkg/mcclient"
	"yunion.io/x/onecloud/pkg/monitor/models"
	"yunion.io/x/onecloud/pkg/monitor/validators"
)

var (
	// ErrFrequencyCannotBeZeroOrLess frequency cannot be below zero
	ErrFrequencyCannotBeZeroOrLess = errors.Error(`"evaluate every" cannot be zero or below`)

	// ErrFrequencyCouldNotBeParsed frequency cannot be parsed
	ErrFrequencyCouldNotBeParsed = errors.Error(`"evaluate every" field could not be parsed`)
)

func init() {
	models.AlertManager.SetTester(NewAlertRuleTester())
}

// Rule is the in-memory version of an alert rule.
type Rule struct {
	Id        string
	Frequency int64
	Title     string
	Name      string
	Message   string
	// 使用 TriggeredMessages 存储触发的条件，替代 Message
	TriggeredMessages   []string
	LastStateChange     time.Time
	For                 time.Duration
	NoDataState         monitor.NoDataOption
	ExecutionErrorState monitor.ExecutionErrorOption
	State               monitor.AlertStateType
	Conditions          []Condition
	Notifications       []string
	// AlertRuleTags       []*models.AlertRuleTag
	Level           string
	RuleDescription []*monitor.AlertRecordRule

	StateChanges int

	CustomizeConfig jsonutils.JSONObject
	// 静默期
	SilentPeriod int64
}

var (
	valueFormatRegex = regexp.MustCompile(`^\d+`)
	unitFormatRegex  = regexp.MustCompile(`\w{1}$`)
)

var unitMultiplier = map[string]int{
	"s": 1,
	"m": 60,
	"h": 3600,
	"d": 86400,
}

func getTimeDurationStringToSeconds(str string) (int64, error) {
	multiplier := 1

	matches := valueFormatRegex.FindAllString(str, 1)

	if len(matches) <= 0 {
		return 0, ErrFrequencyCouldNotBeParsed
	}

	value, err := strconv.Atoi(matches[0])
	if err != nil {
		return 0, err
	}

	if value == 0 {
		return 0, ErrFrequencyCannotBeZeroOrLess
	}

	unit := unitFormatRegex.FindAllString(str, 1)[0]

	if val, ok := unitMultiplier[unit]; ok {
		multiplier = val
	}

	return int64(value * multiplier), nil
}

// NewRuleFromDBAlert maps an db version of
// alert to an in-memory version
func NewRuleFromDBAlert(ruleDef *models.SAlert) (*Rule, error) {
	model := &Rule{}
	model.Id = ruleDef.Id
	model.Title = ruleDef.GetTitle()
	model.Name = ruleDef.Name
	model.Message = ruleDef.Message
	model.TriggeredMessages = make([]string, 0)
	model.State = monitor.AlertStateType(ruleDef.State)
	model.LastStateChange = ruleDef.LastStateChange
	model.For = time.Duration(ruleDef.For)
	model.NoDataState = monitor.NoDataOption(ruleDef.NoDataState)
	model.ExecutionErrorState = monitor.ExecutionErrorOption(ruleDef.ExecutionErrorState)
	model.StateChanges = ruleDef.StateChanges
	model.RuleDescription = make([]*monitor.AlertRecordRule, 0)

	model.Frequency = ruleDef.Frequency
	// frequency cannot be zero since that would not execute the alert rule.
	// so we fallback to 60 seconds if `Frequency` is missing
	if model.Frequency == 0 {
		model.Frequency = 60
	}
	model.CustomizeConfig = ruleDef.CustomizeConfig
	settings, err := ruleDef.GetSettings()
	if err != nil {
		return nil, errors.Wrap(err, "get settings")
	}

	model.Level = ruleDef.Level
	nIds := []string{}
	notis, err := ruleDef.GetNotifications()
	if err != nil {
		return nil, err
	}
	for _, n := range notis {
		noti, _ := n.GetNotification()
		if noti.Frequency != 0 {
			model.SilentPeriod = noti.Frequency
		}
		nIds = append(nIds, n.NotificationId)
	}
	model.Notifications = nIds
	// model.AlertRuleTags = ruleDef.GetTagsFromSettings()
	alert, err := models.CommonAlertManager.GetAlert(ruleDef.Id)
	if err != nil {
		return nil, errors.Wrap(err, "GetCommonAlert error")
	}
	for index, condition := range settings.Conditions {
		condType := condition.Type
		factory, exist := conditionFactories[condType]
		if !exist {
			return nil, errors.Wrapf(validators.ErrAlertConditionUnknown, "condition type %s", condType)
		}
		queryCond, err := factory(&condition, index)
		if err != nil {
			return nil, errors.Wrapf(err, "construct query condition %s", jsonutils.Marshal(condition))
		}
		ruleDesc := alert.GetAlertRule(settings, index, model.SilentPeriod)
		model.RuleDescription = append(model.RuleDescription, ruleDesc)
		model.Conditions = append(model.Conditions, queryCond)
	}

	if len(model.Conditions) == 0 {
		return nil, validators.ErrAlertConditionEmpty
	}
	return model, nil
}

type AlertRuleTester struct{}

func NewAlertRuleTester() models.AlertTestRunner {
	return new(AlertRuleTester)
}

func (_ AlertRuleTester) DoTest(ruleDef *models.SAlert, userCred mcclient.TokenCredential, input monitor.AlertTestRunInput) (*monitor.AlertTestRunOutput, error) {
	rule, err := NewRuleFromDBAlert(ruleDef)
	if err != nil {
		return nil, err
	}
	handler := NewEvalHandler()

	ctx := NewEvalContext(context.Background(), userCred, rule)
	ctx.IsTestRun = true
	ctx.IsDebug = input.IsDebug

	handler.Eval(ctx)

	return ctx.ToTestRunResult(), nil
}

func (ctx *EvalContext) ToTestRunResult() *monitor.AlertTestRunOutput {
	return &monitor.AlertTestRunOutput{
		Firing:             ctx.Firing,
		IsTestRun:          ctx.IsTestRun,
		IsDebug:            ctx.IsDebug,
		EvalMatches:        ctx.EvalMatches,
		AlertOKEvalMatches: ctx.AlertOkEvalMatches,
		Logs:               ctx.Logs,
		Error:              ctx.Error,
		ConditionEvals:     ctx.ConditionEvals,
		StartTime:          ctx.StartTime,
		EndTime:            ctx.EndTime,
		NoDataFound:        ctx.NoDataFound,
		PrevAlertState:     string(ctx.PrevAlertState),
	}
}

// ConditionFactory is the function signature for creating `Conditions`
type ConditionFactory func(model *monitor.AlertCondition, index int) (Condition, error)

var conditionFactories = make(map[string]ConditionFactory)

// RegisterCondition adds support for alerting conditions.
func RegisterCondition(typeName string, factory ConditionFactory) {
	conditionFactories[typeName] = factory
}

func GetConditionFactories() map[string]ConditionFactory {
	return conditionFactories
}
